Wilson@思源

目 录

思源模板功能新人指南:模板语法 + 函数 + md 块语法

推荐教程: 思源模板功能新人指南:模板语法 + 函数 + md 块语法 手把手 数据库模板列简单使用指南 快速预览 https://docs.siyuan-note.club/zh-Hans/reference/template/sprig/ 函数手册 https://docs.siyuan-note.club/zh-Hans/reference/template/siyuan.html 思源专有 https://www.topgoer.com/%E5%B8%B8%E7%94%A8%E6%A0%87%E5%87%86%E5%BA%93/template.html go语言版
思源模板功能新人指南:模板语法 + 函数 + md 块语法 - 链滴
Sy 源文件 思源模板功能新人指南模板语法函数 md 块语法.sy.zip 什么是模板?模板,简单来说就是在一段文本中定义一些「变量」,这些变量在实际渲染的时候会按照一定的规则被替换为实际的值。 比如我们最常见的日记路径模板: /daily note/{{now | date '2006/01'}}/{{now | d
2024-12-10 11:53:33
Sy 源文件
什么是模板?模板,简单来说就是在一段文本中定义一些「变量」,这些变量实际渲染的时候会按照一定的规则被替换为实际的值
比如我们最常见的日记路径模板:
template
/daily note/{{now | date "2006/01"}}/{{now | date "2006-01-02"}}
在每天调用 Alt+5 快捷键创建日记的时候,思源就会渲染以上的模板,把其中由 {{}}​ 内部定义的变量替换为实际的值(在这里是日期),最后变为 /daily note/2024/01/01-31​ 这样具有实际意义的路径字符串。
思源的模板在功能上非常强大,然而实际用起来的感受——作为一个笔记软件的模板而言——老实说,使用起来还挺繁杂的。
这篇文章不会帮你完全掌握思源的模板功能,而是尽量提纲挈领地告诉大家思源模板功能的概况。如果你想要深入了解甚至熟练掌握——那你需要去认真看看相关文档,并抽空练习。
从总体上讲,我会把思源的模板功能划分为这么几个重要的版块:
基于 Golang 的模板语法
基本语法
{{ }}​ vs .action{}
常用的模板函数
常用的流程控制
条件控制
循环控制
数据库中的用法
Markdown 块语法
{: }​ 块属性声明语法
超级块排版声明语法
在正式讲解之前,有必要先把这几个版块之间的关系厘清楚:
1.
Golang 模板语法和 Sprig
思源的模板的语法是基于 Golang 的模板引擎来实现的
Golang 的模板引擎并没有提供多少好用的模板函数,所以思源又内置了 Sprig 库——这个库提供了大量丰富的模板函数,来增强模板的功能性
基本语法
原始的 Golang 模板语法是 {{ xxx }}
但是在思源中 {{ }}​ 又是嵌入块的声明语法
为了避免冲突,开发者就自定义了 .action{ xxx }​ 语法来替换原始的 {{ xxx }}
2.
数据库模板列
思源目前的数据库当中,可以创建一个「模板列」,在这个列当中可以使用 .action{}​ 语法,这样就可以在模板列中根据对应行的内容动态展示其内容
一个类似的例子:就好比在 excel 中增加一列公式
3.
Markdown 块语法
Markdown 本身是没有块结构的;思源实现了一种基于 kramdown 的 markdown 方言;这个方言不属于 Golang 模板语法的一部分,而是属于思源自己的特殊语法
基于这种方言,思源就可以在模板文件中为块预定义自定义属性、通过特殊的语法创建复杂的超级块排版
注意:这个语法和 Golang 模板无关,在数据库 、路径模板的这些地方使用是不会起效果的!

前附:Test-Template 插件

在这篇教程之外,我上架了一个插件“测试模板语法”以方便大家测试 Golang 的模板语法。
你可以在阅读的过程中,使用这个插件对模板语法进行测试。插件的详细用法见说明文档。

思源 Golang 模板语法 Cheatsheet

基本的模板语法讲起来不够直观,这里我画了一个 cheatsheet 给大家展示,信息密度高一点会更加方便理解。

基础语法快速总结

1.
使用 {{ }}​ 或者 .action{}​ 来定义模板,里面一般是函数调用
2.
函数的基本概念
1.
函数可以接受零个或者若干个参数,进行计算后返回一个特定的值
2.
函数的参数和返回值有特定的类型,不同类型之间不能混用
3.
变量的赋值
如果使用 {{ now }}​ 这种语法,模板引擎会直接将其渲染为具体的值
但是如果使用 {{ $t := now}}​ 这种语法,那么 now​ 的返回值会存储在变量 $t​ 中而不会渲染内部具体的值,此后就可以使用 $t​ 来引用这个变量
4.
使用函数
函数是一个封装好的功能包,输入一些参数,进行特定的计算,返回特定的值
传入函数的参数类型要和函数接受的参数类型匹配
|​ 语法:管道语法,这是一种在计算机领域非常常见的语法,相关参考

控制流语法快速介绍

Go 语言模板控制流的用法可以参考官方文档:https://pkg.go.dev/text/template#hdr-Actions
这里介绍最常用的:
1.
if 语句:通过条件判断来有选择性地渲染、执行一部分模板语法
基本的用法是:如果 condition​ 中的条件被判定为 true,则渲染 T1​ 否则渲染 T2
md
{{if (condition)}} T1 {{else}} T0 {{end}}
例如:
md
{{ if eq (Weekday now) 1 }} 好好工作 {{ else }} 摸鱼! {{ end }}
在这里的 condition 中:
1.
首先计算 Weekday now​ 获取今天的星期数
2.
然后通过 eq​ 和 1 对比,看看是不是周一;
1.
如果是,就渲染「好好工作」
2.
如果不是,就渲染「摸鱼!」
如果你不理解这里用到的 eq​ 函数,请不要焦虑,在后面的部分我们会详细说明在 if 语句中常常用到的函数。
2.
range 语句
range 语句被用来在一个序列当中进行迭代,最常见的语法如下
md
{{ range $v := }} {{ $v }} {{ end }}
这里的含义是:
在一个可迭代的 <slice>​ (通常是一个列表 List​)中迭代
每次取出一个值赋值给 $v
我们就可以在 range 块内部访问这个 $v​ 变量
一个简单的案例如下:
md
{{ $list := list 1 2 3 4 }} {{ range $v := $list }} - Index: {{ $v }} {{ end }}
这里我们首先通过 list​ 函数创建一个列表(后面会介绍这个函数),然后在 range​ 中迭代这个列表,依次访问每个值。
渲染结果为:
Index: 1
Index: 2
Index: 3
Index: 4
这里介绍的是最简单的控制流语法,还有更高级的用法请自行探索。

常用函数介绍

函数是想要用好模板功能必定绕不开的一座大山。在上面的 Cheatsheet 中,我们简要介绍过函数的基本概念——函数就是一个封装好的功能包,他可以接收零个或者多个(视函数而定)参数,并计算得到一个输出。
golang
{{ now }} # now 函数,接收 0 个参数,输出当前的时间对象 {{ add 1 2 3 4}} # add 函数,接收任个 int (整数)参数,输出他们的累加结果
思源中支持的函数很多,有 Sprig 包支持的也有思源内置的。而实际在思源中常用的函数,我把他们分类为这几类:
1.
算术计算
整数算术(int 类型)
浮点数算术(float 类型)
2.
时间计算
3.
字符串操作
4.
列表计算
5.
类型转换(偶尔)
6.
逻辑运算(在写条件块的时候要用)
7.
思源模板片段特殊函数
为了方便,我这里整理了最常用的一些函数,进行简单介绍。在介绍的过程中,遵循以下的格式:
Fun​ 代表一个没有参数的函数,比如 now
Fun <int>, <int>​ 代表有几个固定数量参数的函数,比如 sub <int> <int>​;<int>​ 代表了参数的类型,常用类型还有
int​:整数类型
int64​:64 位的整数类型,需要通过类型转换函数和 int​ 类型做转换
float​:浮点数类型
float64​:64 位的浮点数类型,需要通过类型转换函数和 float​ 类型做转换
list​:列表类型
str​:字符串类型
bool​:布尔类型(true​ 或者 false​)
Time​:时间对象类型(这个类型会在后面有更加详细的解释)
Func [<int>,]​ 代表有不定长数量参数的函数,比如 add​ 后面可以跟好多个整数
同时我还会给出一些使用范例,以及渲染后的效果,以供参考。

常用数值计算函数

Sprig 函数
整型 int 计算:完整文档见 https://masterminds.github.io/sprig/math.html
add [<int64>,]​:int64​,累加
sub <int64> <int64>​:int64​,减法
mul [<int64>,]​:int64​,累乘
div <int64> <int64>​ :int64​,整除
mod <int64> <int64>​:int64​,求余
min [<int64>,]​ 和 max [<int64>,]​:int64​,求最小值和最大值
浮点型 float 计算:完整文档见 https://masterminds.github.io/sprig/mathf.html
注:Sprig 的数值运算是在 int64 和 float64 类型上进行的,但是很多函数只接受 int 或 float 类型,所以很多时候要配合类型转换函数来使用,这一点会在下面的小节中详细说明
思源内置数值函数
pow <int>​:指数计算,返回整数
powf <float>​:指数计算,返回浮点数
log <int>​:对数计算,返回整数
logf <float>​:对数计算,返回浮点数
FormatFloat <format str> <n float64>​:string​,说老实话我不能理解这个函数,这是一个老外要求加的,请参考
案例:
template
- add - {{ add 1 2 3 4 }} -> 10 - sub - {{ sub 4 1 }} -> 3 - mul - {{ mul 1 2 3 4 }} -> 24 - div - {{ div 5 2 }} -> 2.5 - mod - {{ mod 5 2 }} -> 1 - min/max - {{ min 5 1 }} -> 1 - {{ max 5 1 }} -> 5 - pow - {{ pow 5 2 }} -> 25 - powf - {{ powf 2.5 2 }} -> 6.25 - log - {{ log 5 2 }} -> 2 - logf - {{ logf 5 2 }} -> 约 2.32 - FormatFloat - {{ FormatFloat "#,###.##" 2345.6789 }} -> 2,345.68
以上的模板会被渲染为:
add
10 -> 10
sub
3 -> 3
mul
24 -> 24
div
2 -> 2.5
mod
1 -> 1
min/max
1 -> 1
5 -> 5
pow
25 -> 25
powf
6.25 -> 6.25
log
2 -> 2
logf
2.321928094887362 -> 约 2.32
FormatFloat
2,345.68 -> 2,345.68

常用的时间函数

Sprig 常用函数
now​:Time​,返回当前的时间
date <fmt str> <Time>​:str​, 将输入的时间对象格式化为字符串
fmt​ 使用 2006-01-02 15:04:05​ 这个固定时间格式(知乎讨论
教你如何记忆这个 🗑️ 垃圾到 😡 爆炸的 magin number
首先年份固定是 2006
后面的月日时分秒从 01 开始依次递增到 05
所以标准格式为:2006-01-02 03:04:05
但是 03:04:05​ 是 12 小时制,所以如果想用 24 小时制,要换算成 2006-01-02 15:04:05
toDate <fmt str> <str>​:Time​ ,将一个字符串转换为一个时间对象
注:思源内置的 parseTime​ 函数使用体验比这个函数要好一点
duratioin <second: int>​:Duration​, 将传入的秒数(int)转换为 Duration​ 对象
思源内置时间函数
ISOWeek <Time>​:int​,返回对应的时间对应今天的第几周
Weekday <Time>​:int​,返回对应的时间是星期几 ;Sunday=0, Monday=1, ..., Saturday=6
WeekdayCN <Time>​:str​,返回对应的时间是星期几;Sunday=日, Monday=一, ..., Saturday=六
WeekdayCN2 <Time>​:str​,返回对应的时间是星期几;Sunday=天, Monday=一, ..., Saturday=六
parseTime <string>​:Time​,解析传入的时间字符串,返回一个时间类型
Time​:Golang 的 time.Time 类型,这个类型里面有不少有用的属性可以访问
完整函数参考https://pkg.go.dev/time#Time
请查找格式为 func (t Time) Func(xxx) XXX​ 的 API 文档
这类函数(属性)都可以在模板中通过 t.Func​ 来调用
Year​: int
Month​: Month​,这是一个枚举类型
虽然显示是英文字符串,但是可以当作数值类型参与计算,例如 add 1 now.Month
Day​: int
Hour​: int
Minute​: int
Second​: int
Sub <Time>​: 计算两个时间之间的差,返回 Duration
Compare <Time>​:比较两个时间对象,返回 int​, -1 或 0 或 1
AddDate <year int> <month int> <day int>​: int​,在当前的时间对象的基础上计算 N 天(月、 年)后的日期(参数可以为负数)
注意,这个函数对月份的处理比较坑,只建议使用 year 和 day 这两个参数
Duration​: Golang 的 time.Duration 类型
请查找格式为 func (d Duration) Func(xxx) XXX​ 的 API 文档
这类函数是可以在模板中通过 t.Func​ 来调用
Hours​:float64​,将 Duration 转换为以小时为单位的数值
Minutes​: float64​,将 Duration 转换为以分钟为单位的数值
Seconds​: float64​,将 Duration 转换为以秒为单位的数值
String​: str​, 将 Duration 按照小时制转换为字符串,显示的格式为 "72h3m0.5s"
案例:
- now - {{ now }} - date - {{ date "2006-01-02 15:04:05" now }} - toDate - {{ toDate "2006-01-02" "2020-01-01" }} - duration - 1800 second: {{ duration 1800 }} - ISOWeek - 第 {{ now | ISOWeek }} 周 - Weekday - 今天是星期: - {{ now | Weekday }} {{ now | WeekdayCN }} {{ now | WeekdayCN2 }} - parseTime - {{ parseTime "2020-01-01 12:00:00" }} - Time 对象 - {{ $t := parseTime "2020-01-01 12:00:00" }} - {{ $t.Year }}/{{ $t.Month }}/{{ $t.Day }} {{ $t.Hour }}:{{ $t.Minute }}:{{ $t.Second }} - {{ now.Sub $t }} - {{ $t.Compare now }} - {{ $t.AddDate 0 0 7}} - Duration 对象 - {{ $du := now.Sub $t }} - {{ $du }} - {{ $du.Hours }}; {{ $du.Minutes }}; {{ $du.Seconds }} - {{ $du.String }}
以上的模板会被渲染为:
now
2024-05-06 22:08:49.9662126 +0800 CST m=+477.037392501
date
2024-05-06 22:08:49
toDate
2020-01-01 00:00:00 +0800 CST
duration
1800 second: 0s
ISOWeek
第 19 周
Weekday
今天是星期:
1 一 一
parseTime
2020-01-01 12:00:00 +0800 CST
Time 对象
2020/January/1 12:0:0
38098h8m49.9662126s
-1
2020-01-08 12:00:00 +0800 CST
Duration 对象
38098h8m49.9662126s
38098.14721283683; 2.28588883277021e+06; 1.371533299662126e+08
38098h8m49.9662126s
日期计算可能是思源用户最常接触到的函数了,如果你是一个 daily note 用户,现在打开你主日记本,查看一下你日记的模板会发现它可能是这个样子:
md
/daily note/{{now | date "2006/01"}}/{{now | date "2006-01-02"}}
现在我们有了理论基础,不妨就这个案例来看一下这个日记模板是怎么回事:
1.
{{}}​ 是 Golang 标准的模板语法,没什么好说的
2.
now​ 函数返回了一个 Time​ 对象
3.
|​ 通过管道运算,把 now​ 的结果传给后面,所以相当于在运行 date "2006/01"
你可以把 {{now | date "2006/01"}​ 换成 {{date "2006/01" now}}​;他们两个是完全等价的
4.
date <fmt str> <Time>​ 是固定搭配的用法,这里 "2006/01"​ 也是固定的用法
5.
所以最后,这个模板会被渲染为 yyyy/mm​ 这样的格式,和前面的组合起来,就会形成 /daily note/<年份>/<月份>​ 这样的路径字符串

分享几个日期计算的模板片段

就经验来看,在正常 md 模板中会写得比较复杂的也只有和日期计算相关的模板了。这里随便分享几个。
可能会用到日记里面的
template
.action{ $weekday := WeekdayCN now } .action{ $datestr := now | date "2006-01-02" } .action{ $datestr_sy := now | date "20060102" } 今天是 .action{$datestr} 星期.action{ $weekday } {: custom-dailynote-${ $datestr_sy }=".action{$datestr_sy}" }
计算这周周日、上周周六等
template
.action{ $weekday := Weekday now } .action{ $begSunday := now.AddDate 0 0 (mul -1 $weekday | int) } .action{ $last6 := $begSunday.AddDate 0 0 -1 } .action{ $this6 := $begSunday.AddDate 0 0 6 } - 如果以周日为一周的起始,那么本周周日为:.action{ $begSunday | date "2006-01-02" } - 上周周六为:.action{ $last6 | date "2006-01-02" } - 本周周六为:.action{ $this6 | date "2006-01-02" }
周规划
template
.action{ $week := now | ISOWeek } .action{ $weekday := now | Weekday } .action{ $monday_delta := sub $weekday 1 } .action{if eq $weekday -1} .action{ $monday_delta := 6 } .action{ end } .action{ $monday := now.AddDate 0 0 (mul -1 $monday_delta | int) } .action{ $sunday := $monday.AddDate 0 0 6 } ## 第 .action{$week} 周: .action{$monday | date "2006-01-02"} ~ .action{$sunday| date "2006-01-02"}

常用字符串操作函数

字符串操作函数在正常 md 模板里面用的没那么多,但是在数据库模板列里面可能会大量用到。
trim <str>​: str​, 将前后的空白字符去掉
repeat <int> <str>​:str​,将给定的字符串重复若干次
substr <start int> <end int> <str>​: str​,提取子字符串,index 从 0 开始
trunc <int> <str>​: str​,将给定的字符串按照最大长度来截断;<int>​ 参数可以是负数,代表从末尾反向截断
abbrev <int> <str>​: str​,同样是截断字符串,但是会在后面加上一个 ...
contains <part str> <whole str>​: bool​,检测 whole​ 中是否包含 part
cat [<str>,]​: str​,将若干字符串拼接起来,中间通过空格间隔
replace <from str> <to str> <src str>​ : str​,将 src​ 中所有的 from​ 替换成 to
正则表达式系列的函数(请自行翻阅文档
join <ch str> <List[str]>​: str​,将给定的字符串列表通过 ch​ 连接起来
splitList <ch str> <src str>​: List[str]​,将给定的 src​ 字符串根据 ch​ 字符分割为列表
案例:
template
- trim - {{ trim " aa " }} -> "aa" - repeat - {{ repeat 5 "12" }} -> "1212121212" - substr - {{ substr 1 3 "abcedfg" }} -> "bc" - trunc - {{ trunc 3 "abcedfg" }} -> "abc" - {{ trunc -3 "abcedfg" }} -> "efg" - abbrev - {{ abbrev 5 "hello world" }} -> "he..." - contains - {{ contains "bb" "aabb" }} -> true - cat - {{ cat "1" "2" "3" }} -> "1 2 3" - replace - {{ replace "aa" "bb" "11aaccaa" }} -> "11bbccbb" - join - {{ list "hello" "siyuan" | join "?" }} -> "hello?siyuan" - splitList - {{ splitList "$" "foo$bar$baz" }} -> ["foo", "bar", "baz"]
渲染结果为:
trim
aa -> "aa"
repeat
1212121212 -> "1212121212"
substr
bc -> "bc"
trunc
abc -> "abc"
dfg -> "efg"
abbrev
he... -> "he..."
contains
true -> true
cat
1 2 3 -> "1 2 3"
replace
11bbccbb -> "11bbccbb"
join
hello?siyuan -> "hello?siyuan"
splitList
[foo bar baz] -> ["foo", "bar", "baz"]

列表操作函数

列表类函数常常会配合 queryBlocks (见后面的小节) 一同使用。
完整文档请见:https://masterminds.github.io/sprig/lists.html
list [<value>,]​:List​,将后面传入的参数变成一个列表(类型要相同)
first <List>​:返回第一个列表项
last <List>​:返回最后一个列表项
append <List> <value>​:List​,在列表后面增加一个元素
prepend <List> <value>​:List​,在列表前面增加一个元素
concat [<List>,]​:List​,将多个列表合并成一个
reverse <List>​:List​,将列表逆序
has <value> <List>​:bool​,检查给定的项目是否在列表中
index <List> <int>​:List​,根据后面的索引值,索引列表中的内容
slice <List> <beg int> {<end int>}​:List​,对给定的列表进行切片
slice $myList​ returns [1 2 3 4 5]​. It is same as myList[:]​.
slice $myList 3​ returns [4 5]​. It is same as myList[3:]​.
slice $myList 1 3​ returns [2 3]​. It is same as myList[1:3]​.
slice $myList 0 3​ returns [1 2 3]​. It is same as myList[:3]​.
empty <List>​:bool​,检查列表是否为空
len <List>​:int​,获取列表的长度
template
- list - {{ list 1 2 3 4 }} -> [1 2 3 4] - {{ list "a" "b" "c" }} -> ["a" "b" "c"] - first - {{ first (list 1 2 3) }} -> 1 - {{ first (list "a" "b" "c") }} -> "a" - last - {{ last (list 1 2 3) }} -> 3 - {{ last (list "a" "b" "c") }} -> "c" - append - {{ append (list 1 2 3) 4 }} -> [1 2 3 4] - {{ append (list "a" "b" "c") "d" }} -> ["a" "b" "c" "d"] - prepend - {{ prepend (list 1 2 3) 0 }} -> [0 1 2 3] - {{ prepend (list "a" "b" "c") "z" }} -> ["z" "a" "b" "c"] - concat - {{ concat (list 1 2) (list 3 4) }} -> [1 2 3 4] - {{ concat (list "a" "b") (list "c" "d") }} -> ["a" "b" "c" "d"] - reverse - {{ reverse (list 1 2 3) }} -> [3 2 1] - {{ reverse (list "a" "b" "c") }} -> ["c" "b" "a"] - has - {{ has 2 (list 1 2 3) }} -> true - {{ has "d" (list "a" "b" "c") }} -> false - index - {{ index (list 1 2 3 4) 2 }} -> 3 - {{ index (list "a" "b" "c" "d") 0}} -> "a" - slice - {{ slice (list 1 2 3 4 5) 1 3 }} -> [2 3] - {{ slice (list "a" "b" "c" "d") 2 }} -> ["c" "d"] - len - {{ list 1 2 3 4 | len }} -> 4
渲染结果为
list
[1 2 3 4] -> [1 2 3 4]
[a b c] -> ["a" "b" "c"]
first
1 -> 1
a -> "a"
last
3 -> 3
c -> "c"
append
[1 2 3 4] -> [1 2 3 4]
[a b c d] -> ["a" "b" "c" "d"]
prepend
[0 1 2 3] -> [0 1 2 3]
[z a b c] -> ["z" "a" "b" "c"]
concat
[1 2 3 4] -> [1 2 3 4]
[a b c d] -> ["a" "b" "c" "d"]
reverse
[3 2 1] -> [3 2 1]
[c b a] -> ["c" "b" "a"]
has
true -> true
false -> false
index
3 -> 3
a -> "a"
slice
[2 3] -> [2 3]
[c d] -> ["c" "d"]
len
4 -> 4

类型转换函数

在思源一个笔记软件里面,纠结数据类型这种纯编程性的问题挺古怪的——但是只要你开始用模板功能,有些时候偏偏就会出现类型兼容性问题。
例如这个例子,他的作用是计算上一个周日的日期(这里我们姑且认为每周从周一开始)。
首先我们使用 Weekday​ 函数获取今天是星期几,数值范围为 0~6
然后调用 AddDate 函数,减去星期数,就获得了周日的日期
{{ $weekday := Weekday now }} {{ now.AddDate 0 0 (mul -1 $weekday) }}
不过运行的这个模板的时候会报错,无法运行:
模板解析失败:template: :2:27: executing "" at <$weekday>: wrong type for value; expected int; got int64 v3.0.12
附:顺便教一下怎么看这个报错信息。
关注 template: :2:27​,这代表了传入的模板的第二行,第二十七个字符的地方出现了错误
具体的错误就是:「wrong type for value; expected int; got int64」,也就是传入的值的类型出了错。
报错的原因在于,mul​ 这些 Sprig 算术函数返回的类型是 int64​,而 Time.AddDate​ 函数接受的类型却是 int​,所以出现了类型不兼容问题。😡
为了解决这一问题,我们不得不进行使用类型转换函数,把 in64 转成 int 类型:
{{ $weekday := Weekday now }} {{ now.AddDate 0 0 (mul -1 $weekday | int) }}
类型转换函数同样是 Sprig 提供的,文档见:https://masterminds.github.io/sprig/conversion.html
atoi:将字符串转换为整数。
float64:转换为float64,即 64 位浮点数值。
int:在系统宽度下转换为int 整形数值。
int64:转换为 64 位的整形数值。
toDecimal:将 Unix 八进制转换为int64。
toString:转换为字符串。
toStrings:将列表、切片或数组转换为字符串列表。
以下是一个简单的案例:
template
- {{atoi "11"}} -> 11 - {{float64 1}} -> 1.0 - {{int "12"}} -> 12 - {{int64 1}} -> 1 - 注意:有些函数的参数只接受 int,而有些只接受 int64,所以某些情况下不得不需要对 int 进行类型转换 - {{toDecimal "20"}} -> 16 - 八进制的 "20" 就是十进制的 16 - {{toString 120}} -> "120" - {{toStrings (list 1 2 3 4)}} -> [1 2 3 4]
渲染为:
11 -> 11
1 -> 1.0
12 -> 12
1 -> 1
注意:有些函数的参数只接受 int,而有些只接受 int64,所以某些情况下不得不需要对 int 进行类型转换
16 -> 16
八进制的 "20" 就是十进制的 16
120 -> "120"
[1 2 3 4] -> [1 2 3 4]

逻辑运算函数

逻辑运算函数,主要是在 if​ 这类流程控制语句内需要用到。比如这个例子当中,eq 就是一个逻辑运算函数,他用来比较传入的两个参数是否相等(equal 的简写)。
template
.action{ if eq 1 2 } 1 .action{ else } 2 .action{ end }
在更加实际的例子中,可能我们要写好几个逻辑运算,建议将每个逻辑运算囊括在 () 当中来保证运算的正确性。
template
.action{ if or (eq 1 2) (ne 2 3) } 1 .action{ else } 2 .action{ end }
逻辑运算部分可以分为两大类:布尔运算类和比较运算类。这里的参数类型 <interface{}>​ 表示是任意类型。
布尔运算
and [<interface{}>,]​:interface{}​,如果所有参数都为真,则返回最后一个参数;否则返回第一个为假的参数
or [<interface{}>,]​:interface{}​,如果任一参数为真,则返回第一个为真的参数;否则返回最后一个参数
not <interface{}>​:bool​,对单个参数进行逻辑非运算;如果参数为真则返回假,为假则返回真
比较运算:对两个类型进行比较,注意这里的参数必须要是同样的类型
eq <interface{}>, <interface{}>​:bool​,判断两个参数是否相等;相等返回真,不等返回假
ne <interface{}>, <interface{}>​:bool​,判断两个参数是否不相等;不相等返回真,相等返回假
lt <interface{}>, <interface{}>​:bool​,比较两个参数,如果第一个小于第二个,则返回真,否则返回假
le <interface{}>, <interface{}>​:bool​,比较两个参数,如果第一个小于等于第二个,则返回真,否则返回假
gt <interface{}>, <interface{}>​:bool​,比较两个参数,如果第一个大于第二个,则返回真,否则返回假
ge <interface{}>, <interface{}>​:bool​,比较两个参数,如果第一个大于等于第二个,则返回真,否则返回假
在常规的逻辑运算函数之外,还有必要介绍一类列表、对象判断类函数,这部份函数由 Default Functions | sprig 提供
empty <interface{}>​:bool​,判断给定的对象是否为空
在思源中,最常见的用法是判断一个列表是不是空的
all [<interface{}>,]​:bool​,判断给定一系列对象,是否每个都是非空的
any [<interface{}>,]​:bool​,判断给定一系列对象,是否存在某一个是非空的
案例如下:
template
- and - {{ and true true }} -> true - {{ and true false }} -> false - {{ and 1 2.3 "str" }} -> "str" - or - {{ or false false }} -> false - {{ or true false }} -> true - not - {{ not true }} -> false - {{ not 0 }} -> true - eq - {{ eq 2 3 }} -> false - {{ eq "a" "a" }} -> true - ne - {{ ne 4 3 }} -> true - {{ ne 2 2 }} -> false - lt - {{ lt 4 3 }} -> false - {{ lt "a" "b" }} -> true - le - {{ le 2 2 }} -> true - {{ le 4 3 }} -> false - gt - {{ gt 5 3 }} -> true - {{ gt 2 2 }} -> false - ge - {{ ge 3 3 }} -> true - {{ ge 1 4 }} -> false - empty - {{ list 1 | empty }} -> false - {{ list | empty }} -> true
and
true -> true
false -> false
str -> "str"
or
false -> false
true -> true
not
false -> false
true -> true
eq
false -> false
true -> true
ne
true -> true
false -> false
lt
false -> false
true -> true
le
true -> true
false -> false
gt
true -> true
false -> false
ge
true -> true
false -> false
empty
false -> false
true -> true

附:使用 ternary 函数计算内联条件

我们上面谈到了最常用的 if 条件语句:
template
.action{ if eq 1 2 } 1 .action{ else } 2 .action{ end }
如果对编程比较有经验的人应该知道,很多语言中都支持计算内联条件,以 js 为例子
js
const x = (1 == 2)? 1 : 2;
在模板函数中,这个内联条件计算可以用 ternary​ 函数来完成,基本用法为:ternary <arg1> <arg2> <cond bool>​:
template
.action{ ternary "Show 1" "Show 2" (eq 1 2) }

思源模板片段特殊函数

有几个思源内置的特殊模板函数在文档里有所介绍,这几个函数****只能用在 md 模板文件当中
title​:该变量用于插入当前文档名。比如模板内容为 # .action{.title}​,则调用后会以一级标题语法插入到当前文档内容中
id​:该变量用于插入当前文档 ID
name​:该变量用于插入当前文档命名
alias​:该变量用于插入当前文档别名
这四个实际上是在「访问正在插入模板的容器文档的一些属性」。
我们可以把下面这个例子保存到 template 目录下的 md 模板文件当中。
md
这个是当前插入文档块的标题:.action{.title} 这个是当前插入文档块的ID:.action{.id} 这个是当前插入文档块的命名:.action{.name} 这个是当前插入文档块的别名:.action{.alias}
然后在某个文档当中插入这个模板,你就会发现这几个字段被替代为被插入的文档块的具体的属性。
这个是当前插入文档块的标题:思源模板片段新人指南 这个是当前插入文档块的 ID:20231014181953-m94pl9u 这个是当前插入文档块的命名: 这个是当前插入文档块的别名:
注意这里使用的时候必须前面要加上 .​,也就是 .title​ 而非 title​。至于为什么要加 .​,有兴趣可以参考这个:Template · Go 语言中文文档

SQL 查询函数

思源还额外提供了两个模板函数,用于做 SQL 查询。
如果你对思源的 SQL 功能不了解,请阅读:思源 SQL 新人指南:SQL 语法 + Query + 模板
queryBlocks​:该函数用于查询数据库,返回值为 blocks 列表,请参考下面的例子
⭐ 这个更常用!
querySpans​:该函数用于查询数据库,返回值为 spans 列表,请参考下面的例子
🤷‍♂️ 这个很少用。
这两个模板函数的返回值都是块的列表。

queryBlocks

queryBlocks​ 可能是最常用的,他的作用就是在 blocks 表中做查询。
md
.action{$blocks := queryBlocks "select * from blocks where 1 limit 1"} 返回结果是列表,这里取出第一个元素:.action{ $b := first $blocks} 块的 ID .action{$b.ID} 块的路径 .action{$b.Path} 完整的块的内容: ‍‍‍‍‍```json .action{toPrettyJson $b } ‍‍‍‍‍```
我们通过 toPrettyJson​ 函数(见 Default Functions | sprig)将结果变成一个 Json,可以发现返回的就是一个 Block 对象。
返回结果是列表,这里取出第一个元素: 块的 ID 20220316145831-f0lgt06 块的路径 /20220316145830-u0u6srg/20220316145830-x6kvftp/20220316145831-f0lgt06.sy 完整的块的内容:
json
{ "ID": "20220316145831-f0lgt06", "ParentID": "", "RootID": "20220316145831-f0lgt06", "Hash": "d7b97e4", "Box": "20220305173526-4yjl33h", "Path": "/20220316145830-u0u6srg/20220316145830-x6kvftp/20220316145831-f0lgt06.sy", "HPath": "/daily note/2022/03", "Name": "", "Alias": "", "Memo": "", "Tag": "", "Content": "03", "FContent": "03", "Markdown": "", "Length": 2, "Type": "d", "SubType": "", "IAL": "{: custom-sy-readonly=\"true\" id=\"20220316145831-f0lgt06\" title=\"03\" type=\"doc\" updated=\"20220316145831\"}", "Sort": 0, "Created": "20220316145831", "Updated": "20220316145831" }
在这里我们使用了如下的写法:
template
.action{$blocks := queryBlocks "select * from blocks where 1 limit 1"}
但是某些情况下,可能 SQL 里面的需要插入一些变量,这种情况下,可以使用 ?​ 占位符来插入变量:
template
.action{$id := "20240507145154-4g8hqau"} .action{$blocks := queryBlocks "select * from blocks where id='?'" $id}

querySpans

querySpans​ 则很少用,他是用来给 spans 表做查询用的
md
.action{$spans := querySpans "select * from spans where 1 limit 1"} ‍‍‍```json .action{toJson $spans } ‍‍‍```
json
[{"ID":"20240420235208-65m9ro6","BlockID":"20220324170238-169kwsw","RootID":"20220324170216-lp8jviy","Box":"20220305173526-4yjl33h","Path":"/20220316145830-u0u6srg/20220316145830-x6kvftp/20220316145831-f0lgt06/20220324170216-lp8jviy.sy","Content":"随想录","Markdown":"((20220320164548-ienx5sl '随想录'))","Type":"textmark block-ref","IAL":""}]

案例:链接到昨天的日记

querySpans 且不谈,配合 queryBlocks 我们可以在模板中玩一些奇妙的花活。本小节作为模板语法部分的收尾,给大家介绍一个有趣的案例。
前两天我看有人在论坛里问:日记模板怎样设置可以在今天日记页面中自动添加昨天的日记链接?
他提得需求就完全可以通过 queryBlocks 来完成。
要实现这个功能,必须知道两个前置知识:
1.
在思源里,日记文档会自动添加 custom-dailynote-<yyyymmdd>​ 属性;比如 2024-05-01 的日记,会自动添加文档属性 custom-dailynote-20240501
2.
思源中,超链接的格式为 siyuan://blocks/<块 ID>
有了这两个知识,我们就有大致的实现思路了:
1.
获取昨天的日期
2.
根据昨天的日期构建日记文档属性
3.
查询符合文档属性的文档
4.
如果查询到,就去除文档的 ID 并构建块链接
首先要获得 yyyymmdd​ 属性,这个我在「常用的时间函数」当中已经给出写法了:
template
.action{ $datestr_sy := now | date "20060102" } {: custom-dailynote-${ $datestr_sy }=".action{$datestr_sy}" }
这里我们要把 now​ 替换成昨天,改成这样以下这样;其中用到了 list、join 两个函数,我在前面也介绍过。
template
.action{ $datestr_sy := now.AddDate 0 0 -1 | date "20060102" } .action{ $attr := list "custom-dailynote-" $datestr_sy | join "" }
接下来我们就要在模板中调用 queryBlock 来查询昨天的日记:
template
.action{ $datestr_sy := now.AddDate 0 0 -1 | date "20060102" } .action{ $attr := list "custom-dailynote-" $datestr_sy | join "" } .action{ $docs := queryBlocks "select * from blocks where type='d' and ial like '%custom-dailynote-20240506%' limit 1"}
获得了 $docs​ 我们需要做以下的工作:
1.
首先判断列表是否为空(用 empty 函数),因为很可能昨天没有写日记
2.
如果不为空,就使用 first​ 函数取出第一个文档元素
3.
通过 .ID​ 获取文档的 ID,并构造 markdown 链接
最后完整的模板内容如下:
template
.action{ $datestr_sy := now.AddDate 0 0 -1 | date "20060102" } .action{ $attr := list "custom-dailynote-" $datestr_sy | join "" } .action{ $docs := queryBlocks "select * from blocks where type='d' and ial like '%custom-dailynote-20240506%' limit 1" } .action{ if not (empty $docs) } .action{ $doc := first $docs } [.action{$doc.Content}](siyuan://blocks/.action{ $doc.ID }) .action{ end }

数据库中的模板列

思源的数据库中,有一种类型的列叫做「模板列」,通过模板列,我们可以让数据库发挥出更加强大的功能。
口说无凭,来看一个数据库的效果:
在这个数据库当中,「评级」是一个模板列,他会根据左边打分的数值,自动调整显示为不同的星星数量。那么这是怎么做到的呢?
1.
首先新建评级列,将它设置为一个模板列
2.
编辑模板为
md
.action{ $scale := (div .打分 20) } .action{ repeat (int $scale) "⭐" }
看起来非常简单!你应该能记得,我们之前讨论过这里用到的三个函数:
div​:进行除法运算,并将结果赋值给 $scale​ 变量
由于 div​ 的结果类型为 int64​ 所以通过 int​ 函数把他转为整型
通过调用 repeat​ 函数,根据 $scale​ 变量的数值来重复显示 ⭐ 符号
插件测试样例,注:由于这里是在插件当中测试,无法访问 .打分​ 属性,所以用常量 50 来替换以完成测试。
当然以上都不是重点,这里的重点是—— .打分​ 是什么,为什么我们可以访问他?

.​ 对象

也许你还记得,.​ 符号在此前也出现过很多次。比如之前的 now.Year​ 就表示:
通过函数 now​ 返回的对象 Time
对对象 Time​ 使用 .​ 获取他的属性 Year
同样在这里 .​ 符号也表示获取属性。但是由于之前没有指定被索引的对象,所以这里使用了传入模板的默认数据对象——在思源数据库中,他就对应了模板列单元所在的****行本身
例如一个数据库拥有 日期​、分数​、备注​ 这些列,我们就可以通过 .日期​、.分数​、.备注​ 在模板列中访问他们。
那么除了列本身,还有可以访问的对象吗?
当然有!如果你想知道更加细节的可访问字段,这里有一个简单的技巧:
在数据库中新建一个模板列
将模板设置为 .action{toPrettyJson .}
将模板列设置为「换行」
然后你就可以在模板列中看到本数据库中的所有可访问字段了,比如通过 .created​ 访问行创建的时间。
但是注意,并不是所有的属性都可以通过 .​ 来访问,比如如果我们尝试访问一个名为 .custom-b​ 的对象,那么会喜提一个报错——因为 custom-b 不符合属性的命名规范。
对于这种情况,可以通过 index . "name"​ 来对属性进行访问,比如这个例子中,我们填写 .action{ index . "custom-b"}​ 就可以访问 custom-b​ 属性的值了。
附注:
模板使用关联或汇总时,填充值为数组,所以可能需要使用 index .汇总 0​ 来访问第一个值,或者使用 range​ 迭代所有值。

两种主键

思源的数据库中的主键可以分为两种:
1.
普通的文本主键
2.
被绑定到一个块的主键
为了方便测试,我们现在新建一个测试用的块,并填写命名、属性、备注还有自定义属性:
以下是不同的两行内部属性的差异:
1.
普通文本主键只有 id、update、created、主键等属性
2.
绑定了块的主键,还多出来
数据库相关的属性,如 av-names、custom-avs 等
块的内置属性,如命名、别名等
自定义属性,如 "custom-a"
3.
绑定了块的主键,其 ID 指向了被绑定的块,而普通文本主键的 ID 是它自身的 ID
4.
注意:普通的文本主键的 ID 并非指向一个块,所以如果你尝试用 SQL 来查询这个 ID 对应的块,是不会得到任何有意义的结果的!

和 queryBlocks 结合

对于主键绑定了块的数据库而言,由于 ID 属性和对应的块打通了沟通渠道,所以把模板列和 queryBlocks 方法结合玩一些更加花哨的功能。
这里给出一个简单的案例:自动获取绑定块的文档的标题
1.
首先使用 queryBlocks 执行 SQL 查询获取对应的文档块,SQL 语句的设计如下:
sql
select * from blocks where id in (select root_id from blocks where id='')
2.
通过 if 语句过滤掉不存在文档块的情况
1.
通过 first​ 函数取出第一个文档块
2.
然后获取它的 Content 字段
3.
对于不存在文档块的情况,简单输出一个「无」
template
.action{ $blocks := queryBlocks "select * from blocks where id in (select root_id from blocks where id='?')" .id} .action{ if not (empty $blocks)} .action{ (first $blocks).Content } .action{ else } 无 .action{ end }
效果如下:
论坛里有一个帖子,询问:数据库一些列的值能否自动获取文档里的数据。其实解决思路和上面的是一样的:
1.
获取绑定块的 ID
2.
构造 SQL,查询到想要的内容
3.
读取 queryBlocks 的结果,并写入数据库模板列中

和 html 标签结合

有一个很重要,但是可能很少人知道的事情:数据库的文本列里面是可以直接填写 html 元素的
举一个例子,我们把如下的 html 粘贴到一个文本列当中:
html
75%
然后,这个元素就会被完整地渲染在数据库单元内:
坏消息是:当你再次点击编辑的时候,里面就只剩下纯文本了。
🤨 更坏的消息:可能未来某个版本内,文本列不会再支持插入 html 了。
好消息是:模板列里面也支持使用自定义的 html 元素。我们不妨再做一个测试:
1.
新建一个数值列“比例”,取值范围从 0 ~ 100
2.
将如下的模板粘贴到一个模板列当中,他做的事情很简单:读取「比例」的数值,然后替换到 div 元素当中
html
.action{ $portion := index . "比例"}
.action{$portion}%
做完以上的工作,数据库就会根据 "比例" 这一列的数值来绘制模板列的样式,从而显示出不同的效果出来:

奇奇怪怪的玩法(存在风险,慎用)

拓展性花活,普通用户慎用
自定义 html + 可执行的 js 是存在安全风险的,这里只是通过这个例子告诉大家数据库可以做到这种程度。
如果你不能百分百确定你在干什么,就不要这么玩。
先给大家看一个神奇的东西:
在前面我们提到过,数据库的文本实际上是可以直接插入 html 标签的——那既然可以插入 html 标签,那执行一下 js 代码也是非常合理的对吧。
现在我来解释一下上面这个效果的实现方案:
1.
首先将数据库的主键绑定到一个块上——从而保证使用 .id​ 能访问到块的 id
2.
新建一个 css 文本列,用来填写具体的 css 属性表
3.
新建一个模板列,里面填写如下内容
html
我们来分析一下这个模板:
<button style="margin: 3px;" class="b3-button" />​ 构建了一个按钮元素, b3-button​ 是思源的内置 class 名称
关键是 onclick 元素,我们在 onclick 当中直接填写我们要执行的代码:
1.
event.stopImmediatePropagation();
这行代码是为了阻止思源默认的数据表行为,如果去掉了以后,思源就会弹出一个编辑框,而不会触发点击按钮的事件了
2.
runJs.api.setBlockAttrs
这个其实是安装了 RunJs​ 插件后,插件暴露在外部的一个接口
setBlockAttrs 是思源的内核 API,用于为块设置块属性
你不一定要安装 runJs 插件,也可以在 JS 代码片段中添加自己想要使用的代码功能
3.
setBlockAttrs 接受两个参数:块的 ID,具体的块属性
1.
块 ID:通过模板功能的 .action{.id}​ 获取
2.
块属性:由于我们要设置内联样式属性,所以 API 的 payload body 填写为 {style: "<具体css代码>"}
使用 .action{index . "css"}​ 获取 css 列的内容,填充到 payload body 中

思源 Markdown 块语法

严格来说,思源的 Markdown 块语法和模板语法没有半毛钱关系。但是由于在 md 模板文件里面经常需要使用 md 块语法,所以有必要在这里一起介绍一下。
我们都知道思源是以块为单位的,同时思源一定程度上兼容了 markdown 语法格式(例如在模板文件中)——那么问题来了,markdown 原始的语法就是个瘸子,根本不存在块的概念,更不用提块属性这种概念了——这可怎么办呢。
对于最普通的情况来说来讲,一个 markdown 内置的语法元素,例如标题、列表、引述等等,也会被转化为一个单独的块。
而对于更加复杂的情况,思源实现了一种基于 kramdown 的 markdown 方言。通过这个方言我们可以在一个普通的 markdown 纯文本文件中定义块及其内部的属性。
思源的 markdown 这些拓展属性语法,不仅仅可以用在模板的 md 文件里, 还可以直接用在编辑器中。
在阅读下面的示例的时候,你可以把给出的 md 模板样例复制之后直接粘贴到思源的编辑器中,思源同样能够正确识别定义的块属性。

块属性声明语法

思源可以通过 {: }​ 这种内联样式表 (简称:ial) 的形式来声明一个具备特定属性的块,内联样式表的基本语法格式为如下,注意括号两侧必须留出空格。
latex
{: ="1" ="1" }
例如,你可以把以下的文本填入 templates​ 下的 Test.md 文件:
md
这是一个简单的段落,但是我会声明「备注」内联样式和「custom-a」自定义属性。 {: memo="这是一个块" custom-a="a" }
而将这个模板应用到思源正文后,就会创建一个填充了备注和 custom-a 属性的块。
再比如,在使用了 callout (由 Savor 主题以及 callout 插件提供)功能后,想要通过模板快速插入一个 callout——通过观察发现 callout 样式主要依赖于对一个引述块添加 custom-b​ 属性,所以我们可以首先在 Test.md 中编写引述块的 markdown 语法,然后在下方紧贴着添加 {: }​ 属性表:
md
> Meta > - 🚩 文档类型:主题文档 | 事件记录 > - ⭐ 相关主题: > - 📝 基本介绍: {: custom-b="info" }
注 :可以使用的块属性包括了块的内置属性自定义属性
1.
可以使用内置样式包括:id, name, alias, bookmark, memo, style
2.
在设置自定义样式的时候,不要忘了添加 custom-​ 前缀

Special Case: ID 属性

我们可以在 ial 表中定义 ID 属性,比如这样:
md
这是一个块,你猜他的 ID 会是什么? {: id="20231004221035-p9r4sh7"}
然而,在 ial 表中定义的 ID 属性并不会真正应用到编辑器的块中——也就是说就算使用了上述的模板,新创建的块的 ID 并不会等于 "20231004221035-p9r4sh7",而是思源自己生成的新的块 ID。
id 属性在 ial 中唯一的作用是作为 ial 中的占位符来声明一个块——因为一个空的 ial 表是无效的。
听起来像是脱裤子放屁——确实,在大部分情况下,这个 trick 都没有什么用。
但是在处理复杂容器块的时候,我们会不得不使用这个 trick 来区分容器块和内部的内容块。这里给一个例子:
假定:我们需要编写一个内容为空的列表块,并给这个列表块添加 style="border: 1px solid blue;"​ 属性(也就是添加外边框的内联 css 样式)。请问要怎么办呢?
你可能会写出如下的模板样式
md
- {: style="background: red;" }
但是以上的模板是无效的。
原因在于,思源中的列表块是一个非常复杂的嵌套对象,一个最简单的单个项目的列表,实际上也包含了三个部分:
最内部的段落块
段落块外部的列表项块
列表项从属的列表块
而在以上的模板中,思源无法识别 {: style="background: red;" }​ 到底是应用在哪个块上面。
在这种情况下,我们必须要通过类似 {: id="20231004221035-p9r4sh7"}​ 这样的样式表,来明确声明三层块结构,也就是要写成这样子:
md
- {: id="202001010000-abcdefg"} {: id="202001010000-abcdefg"} {: style="border: 1px solid blue;" }
另外虽然 id​ 的值实际上没有什么卵用,但是在填写的时候必须要遵守 ID 的格式规范,否则无法被正常识别:
md
yyyymmddHHMMSS-<7位字符或数字>
诸如 "202001010000-abcdefg"、"202001010000-1234567"这些都是可以的,而 "123" 这种是不行的。

超级块排版

思源支持通过超级块来进行复杂排版,这个排版同样可以通过拓展的 markdown 语法来定义:
多行超级块(垂直布局)
md
{{{row <内部内容> }}}
多列超级块(水平布局)
md
{{{col <内部内容> }}}
具体编写的时候,只需要把 < 内部内容 > 当中替换为正常的块定义即可。然后渲染的时候,内部内容就会按照垂直或者水平布局被囊括在一个新的超级块当中。
这里给一个稍微复杂一点的样例,如果你把这个样例理解了,超级块排版的布局基本上就掌握了。
md
{{{col {{{row ### TODO:高优先 - [x] }}} {: style="border: 2px solid blue;"} {{{row ### TODO:低优先 - [x] }}} {: style="border: 2px solid blue;"} }}} {: style="border: 2px solid red; padding: 10px;"}
插入编辑器后,样式如下:
最外部是一个水平布局的超级块,内部有两列垂直布局的超级块
内部的两个列中,各自都是一个垂直布局的超级块,包括
最上面一个标题块
最下面一个任务块

相关参考资料